﻿#include "StdAfx.h"
#include "FrameBuffer.h"
#include "DisplaySettings.h"
#include "TerminalSettings.h"
#include "Coordinate.h"
#include "BbsConsole.h"
#include <Imm.h>

IMPLEMENT_DYNAMIC(CBbsConsole, CWnd)

BEGIN_MESSAGE_MAP(CBbsConsole, CWnd)
	ON_WM_CREATE()
	ON_WM_PAINT()
	ON_WM_TIMER()
	ON_WM_CLOSE()
	ON_WM_CHAR()
	ON_WM_KEYDOWN()
	ON_MESSAGE(WM_TEXTRECEIVED, OnTextReceived)
	ON_MESSAGE(WM_ANSICOMMAND, OnAnsiCommand)
	ON_MESSAGE(WM_IME_STARTCOMPOSITION, OnImeStartComposition)
END_MESSAGE_MAP()

CBbsConsole::CBbsConsole(void) : 
	displaySettings(CDisplaySettings::Default), 
	terminalSettings(CTerminalSettings::Default),
	blinkingTimer(0), 
	blinkingToggle(true), 
	wideCharConversionBuffer(NULL), 
	codepage(54936) // GB2312
{
}

CBbsConsole::~CBbsConsole(void)
{
	delete wideCharConversionBuffer;
}

const int CBbsConsole::Rows() const
{
	return this->displaySettings.Rows();
}

const int CBbsConsole::Columns() const
{
	return this->displaySettings.Columns();
}

BOOL CBbsConsole::PreCreateWindow(CREATESTRUCT& cs)
{
	cs.style &= ~WS_BORDER;
	return CWnd::PreCreateWindow(cs);
}

int CBbsConsole::OnCreate(LPCREATESTRUCT)
{
	this->frameBuffer.CreateBuffer(this->Rows(), this->Columns());
	RefreshDisplaySettings();
	Reset();

	VERIFY(0 != (this->blinkingTimer = SetTimer(1, 500, NULL)));
	
	return 0;
}

void CBbsConsole::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	unsigned char* buffer;
	int count;

	if (IsWide(nChar))
	{
		count = WideCharToMultiByte(this->codepage, 0, (LPCWSTR) &nChar, 2, NULL, 0, NULL, NULL);
		buffer = new unsigned char[count];

		WideCharToMultiByte(this->codepage, 0, (LPCWSTR) &nChar, 2, (LPSTR) buffer, count, NULL, NULL);
	}
	else
	{
		count = 1;
		buffer = new unsigned char[count];
		buffer[0] = (unsigned char) nChar;
	}

	OnInputReceivedEvent(buffer, count);
}

void CBbsConsole::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	unsigned char* buffer;
	int count;
	if (this->terminalSettings.MapKeyToSequence(nChar, buffer, count))
	{
		OnInputReceivedEvent(buffer, count);
	}
	else
	{
		CWnd::OnKeyDown(nChar, nRepCnt, nFlags);
	}
}

void CBbsConsole::OnPaint()
{
	CPaintDC dc(this);
	CRect rect;
	GetClientRect((LPRECT) &rect);

	for (int r = 0; r < this->Rows(); r++)
		for (int c = 0; c < this->Columns(); c++)
			PaintSquare(dc, r, c);
}

void CBbsConsole::OnTimer(UINT_PTR nIDEvent)
{
	this->blinkingToggle = !this->blinkingToggle;
	for (int r = 0; r < this->Rows(); r++)
	{
		for (int c = 0; c < this->Columns(); c++)
		{
			unsigned char attributes = this->frameBuffer(r, c).Attributes;
			if (CFrameBuffer::HasBlink(attributes))
				InvalidateSquare(r, c);
		}
	}

	InvalidateSquare(this->cursorPosition);
	CWnd::OnTimer(nIDEvent);
}

void CBbsConsole::OnClose()
{
	if (this->blinkingTimer != 0)
		KillTimer(this->blinkingTimer);

	CWnd::OnClose();
}

LRESULT CBbsConsole::OnTextReceived(WPARAM wParam, LPARAM lParam)
{
	int count = (int) wParam;
	unsigned char* buffer = (unsigned char*) lParam;

	WriteText(buffer, count);
	delete[] buffer;

	return 0;
}

LRESULT CBbsConsole::OnAnsiCommand(WPARAM, LPARAM lParam)
{
	CAnsiCommand* command = (CAnsiCommand*) lParam;
	ExecuteAnsiCommand(*command);
	delete command;

	return 0;
}

LRESULT CBbsConsole::OnImeStartComposition(WPARAM wParam, LPARAM lParam)
{
	HIMC hIMC = ImmGetContext(this->m_hWnd);
	
	if (hIMC != NULL)
	{
		COMPOSITIONFORM form;
		form.dwStyle = CFS_POINT;
		form.ptCurrentPos.x = this->cursorPosition.column * this->charWidth;
		form.ptCurrentPos.y = this->cursorPosition.row * this->charHeight;
		ImmSetCompositionWindow(hIMC, &form);
		ImmReleaseContext(this->m_hWnd, hIMC);
	}

	return CWnd::DefWindowProc(WM_IME_STARTCOMPOSITION, wParam, lParam);;
}

void CBbsConsole::RefreshDisplaySettings()
{
	// Create fonts
	LPCTSTR fontName = this->displaySettings.FontName();
	int fontSize = this->displaySettings.FontSize();
	CPaintDC dc(this);
	
	// TODO: support for other CHARSET?
	VERIFY(this->normalFont.CreateFont(
		fontSize,					// nHeight
		0,							// nWidth
		0,							// nEscapement
		0,							// nOrientation
		FW_NORMAL,					// nWeight
		FALSE,						// bItalic
		FALSE,						// bUnderline
		0,							// cStrikeOut
		GB2312_CHARSET,				// nCharSet
		OUT_DEFAULT_PRECIS,			// nOutPrecision
		CLIP_DEFAULT_PRECIS,		// nClipPrecision
		DEFAULT_QUALITY,			// nQuality
		FIXED_PITCH | FF_DONTCARE,	// nPitchAndFamily
		fontName					// lpszFacename
		));

	VERIFY(this->underlineFont.CreateFont(fontSize, 0, 0, 0, FW_NORMAL, FALSE, TRUE, 0, 
		GB2312_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, 
		FIXED_PITCH | FF_DONTCARE, fontName));

	// Calculate charWidth and charHeight.
	dc.SelectObject(&this->normalFont);
	CSize size = dc.GetTextExtent(_T("W"), 1);
	this->charWidth = size.cx;
	this->charHeight = size.cy;

	// TODO: Hmm, how to notify parent window to reposition?

	// Redraw the whole window.
	Invalidate(FALSE);
}

void CBbsConsole::GetPreferredWindowSize(CSize& size)
{
	// TODO: border size? or shall we disable borders at all?
	size.cx = this->charWidth * this->Columns();
	size.cy = this->charHeight * this->Rows();
}

void CBbsConsole::Reset()
{
	this->cursorPosition.column = 0;
	this->cursorPosition.row = 0;
	this->showCursor = true;

	this->backgroundColor = Black;
	this->foregroundColor = White;
	this->attributes = 0;

	ExecuteEraseInDisplay(2);
}

void CBbsConsole::ExecuteAnsiCommand(const CAnsiCommand& command)
{
	switch (command.action)
	{
	case CAnsiCommand::SelectGraphicRendition:
		if (command.parameterCount == 0)
			ExecuteSelectGraphicRendition(0);
		else
			for (int i = 0; i < command.parameterCount; i ++)
				ExecuteSelectGraphicRendition(command.parameterBuffer[i]);
		break;

	case CAnsiCommand::SaveCursorPosition:
		this->savedCursorPosition = this->cursorPosition;
		break;

	case CAnsiCommand::DeviceStatusReport:
		char s[16];
		sprintf_s(s, "\x1b[%d;%dR", this->cursorPosition.row+1, this->cursorPosition.column+1);
		WriteText((unsigned char*)s, strlen(s));
		break;

	case CAnsiCommand::EraseInLine:
		ExecuteEraseLine(command.parameterBuffer[0]);
		break;

	case CAnsiCommand::EraseInDisplay:
		ExecuteEraseInDisplay(command.parameterBuffer[0]);
		break;

	case CAnsiCommand::InsertLine:
		this->frameBuffer.InsertLine(this->cursorPosition.row);
		InvalidateBlock(this->cursorPosition.row, 0, Rows()-1, Columns()-1);
		break;

	default:
		// Cursor movement commands
		InvalidateSquare(this->cursorPosition);

		if (command.action == CAnsiCommand::CursorUp)
		{
			this->cursorPosition.row -= command.parameterBuffer[0];
		}
		else if (command.action == CAnsiCommand::CursorDown)
		{
			this->cursorPosition.row += command.parameterBuffer[0];
		}
		else if (command.action == CAnsiCommand::CursorForward)
		{
			this->cursorPosition.column += command.parameterBuffer[0];
		}
		else if (command.action == CAnsiCommand::CursorBack)
		{
			this->cursorPosition.column -= command.parameterBuffer[0];
		}
		else if (command.action == CAnsiCommand::CursorNextLine)
		{
			this->cursorPosition.row += command.parameterBuffer[0];
			this->cursorPosition.column = 0;
		}
		else if (command.action == CAnsiCommand::CursorPrevLine)
		{
			this->cursorPosition.row -= command.parameterBuffer[0];
			this->cursorPosition.column = 0;
		}
		else if (command.action == CAnsiCommand::CursorPosition)
		{
			this->cursorPosition.row = command.parameterBuffer[0]-1;
			this->cursorPosition.column = command.parameterBuffer[1]-1;
		}
		else if (command.action == CAnsiCommand::RestoreCursorPosition)
		{
			this->cursorPosition = this->savedCursorPosition;
		}
		else
		{
			ASSERT(FALSE); // ANSI command not implemented
		}

		ASSERT(this->cursorPosition.row >= 0 && this->cursorPosition.row < Rows());
		ASSERT(this->cursorPosition.column >= 0 && this->cursorPosition.column < Columns());

		InvalidateSquare(this->cursorPosition);
	}
}

void CBbsConsole::ExecuteSelectGraphicRendition(int code)
{
	switch (code)
	{
	case 0:
		this->foregroundColor = White;
		this->backgroundColor = Black;
		this->attributes = 0;
		break;
	case 1:
		this->attributes |= Bold;
		break;
	case 4:
		this->attributes |= Underline;
		break;
	case 5:
		this->attributes |= Blink;
		break;
	case 7:
		this->attributes |= Inverse;
		break;
	case 30:
	case 31:
	case 32:
	case 33:
	case 34:
	case 35:
	case 36:
	case 37:
		this->foregroundColor = (AnsiColor)(code - 30);
		break;
	case 40:
	case 41:
	case 42:
	case 43:
	case 44:
	case 45:
	case 46:
	case 47:
		this->backgroundColor = (AnsiColor)(code - 40);
		break;
	default:
		ASSERT (FALSE); // Graphic rendition code not implemented
	}
}

void CBbsConsole::ExecuteEraseInDisplay(int param)
{
	switch (param)
	{
	case 0:
		// clear from cursor to end of screen
		for (int c = this->cursorPosition.column; c < Columns(); c++)
			SetCharAt(this->cursorPosition.row, c, L' ');

		for (int r = this->cursorPosition.row + 1; r < Rows(); r++)
			for (int c = 0; c < Columns(); c++)
				SetCharAt(r, c, L' ');
		break;

	case 1:
		// clear from cursor to beginning of screen
		for (int c = this->cursorPosition.column; c >= 0; c--)
			SetCharAt(this->cursorPosition.row, c, L' ');

		for (int r = this->cursorPosition.row - 1; r >= 0; r--)
			for (int c = Columns()-1; c >= 0; c--)
				SetCharAt(r, c, L' ');
		break;

	case 2:
		// clear the whole screen
		for (int c = 0; c < Columns(); c++)
			for (int r = 0; r < Rows(); r++)
				SetCharAt(r, c, L' ');
		break;
	default:
		ASSERT(FALSE); // Invalid EraseInDisplay parameter.
	}
}

void CBbsConsole::ExecuteEraseLine(int param)
{
	switch (param)
	{
	case 0:
		// clear from cursor to end of line
		for (int c = this->cursorPosition.column; c < Columns(); c++)
			SetCharAt(this->cursorPosition.row, c, L' ');
		break;

	case 1:
		// clear from cursor to beginning of line
		for (int c = this->cursorPosition.column; c >= 0; c--)
			SetCharAt(this->cursorPosition.row, c, ' ');
		break;

	case 2:
		for (int c = 0; c < Columns(); c++)
			SetCharAt(this->cursorPosition.row, c, ' ');
		break;

	default:
		ASSERT(FALSE); // Invalid ExecuteEraseLine parameter
	}
}

void CBbsConsole::WriteText(unsigned char* inputBuffer, int inputCount)
{
	LPWSTR p;
	int n;
	VERIFY(ToWideChar((LPCSTR)inputBuffer, inputCount, p, n));
	
	bool oldShowCursor = this->showCursor;
	this->showCursor = FALSE;
	InvalidateSquare(this->cursorPosition);
	for (int i = 0; i < n; i++)
	{
		wchar_t ch = p[i];
		if (!iswcntrl(ch))
		{
			SetCharAt(this->cursorPosition.row, this->cursorPosition.column ++, ch);
			if (IsWide(ch))
				this->cursorPosition.column ++;

			if (this->cursorPosition.column >= Columns())
				this->cursorPosition.column = Columns()-1;
		}
		else
		{
			InvalidateSquare(this->cursorPosition);
			switch (ch)
			{
				case L'\n':
					if (this->cursorPosition.row == Rows()-1)
					{
						this->frameBuffer.ScrollUp();
						InvalidateBlock(0, 0, Rows()-1, Columns()-1);
					}
					else
						this->cursorPosition.row ++;
					break;
				case L'\r':
					this->cursorPosition.column = 0;
					break;
				case L'\b':
					if (this->cursorPosition.column > 0)
					{
						this->cursorPosition.column --;
						InvalidateSquare(this->cursorPosition);
					}
					break;
			}
		}
	}

	this->showCursor = oldShowCursor;
	InvalidateSquare(this->cursorPosition);
}

void CBbsConsole::SetCharAt(int row, int column, wchar_t ch)
{
	this->frameBuffer.SetCharAttributeAt(row, column, CharAttribute (ch, this->foregroundColor, this->backgroundColor, this->attributes));
	InvalidateSquare(row, column);

	if (IsWide(ch) && column+1 < this->Columns())
	{
		this->frameBuffer.SetCharAttributeAt(row, column+1, CharAttribute (0, this->foregroundColor, this->backgroundColor, this->attributes));
		InvalidateSquare(row, column+1);
	}
}

bool CBbsConsole::ToWideChar(LPCSTR inputBuffer, int inputCount, LPWSTR& outputBuffer, int& outputCount)
{
	int len = MultiByteToWideChar(this->codepage, 0, inputBuffer, inputCount, NULL, 0);

	if (this->wideCharConversionBuffer == NULL || len > this->wideCharConversionCount)
	{
		if (this->wideCharConversionBuffer != NULL)
			delete this->wideCharConversionBuffer;

		// Allocate the next power of 2, but at least 1024 bytes
		int c = 1024;
		while (c < len)
			c <<= 1;

		this->wideCharConversionBuffer = new wchar_t[c];
		VERIFY(this->wideCharConversionBuffer != NULL);
		this->wideCharConversionCount = c;
	}

	if (MultiByteToWideChar(codepage, 0, inputBuffer, inputCount, this->wideCharConversionBuffer, len) > 0)
	{
		outputCount = len;
		outputBuffer = this->wideCharConversionBuffer;
		return true;
	}
	else
	{
		outputCount = 0;
		outputBuffer = NULL;
		return false;
	}
}

void CBbsConsole::PaintSquare(CPaintDC &dc, int row, int column)
{
	CharAttribute square = this->frameBuffer(row, column);
	wchar_t ch = square.Character;

	if (ch == 0)
		return;

	// Choose font based on Underline bit
	if (CFrameBuffer::HasUnderline(square.Attributes))
		dc.SelectObject(&this->underlineFont);
	else
		dc.SelectObject(&this->normalFont);

	// Determine if it should be drawn inversed based on Inverse bit and if it's selected
	bool inverse = CFrameBuffer::HasInverse(square.Attributes) ^ IsSelected(row, column);

	// Determine if it should be drawn as a cursor
	bool isCursor = this->blinkingToggle && this->showCursor && row == this->cursorPosition.row && column == this->cursorPosition.column;

	// Choose a palette for foreground based on Bold bit
	const COLORREF* fgPalette = CFrameBuffer::HasBold(square.Attributes) ? this->displaySettings.BoldPalette() : this->displaySettings.Palette();

	COLORREF fgColor = fgPalette [inverse ? square.Background : square.Foreground];
	COLORREF bgColor = isCursor ? this->displaySettings.CursorColor() : this->displaySettings.Palette()[inverse ? square.Foreground : square.Background];

	dc.SetTextColor(fgColor);
	dc.SetBkColor(bgColor);
	
	int x = column * this->charWidth;
	int y = row * this->charHeight;

	bool wide = IsWide(ch) && column+1 < this->Columns();
	int w = this->charWidth*(wide?2:1);
	int h = this->charHeight;

	dc.FillSolidRect(x, y, w, h, bgColor);

	if (!CFrameBuffer::HasBlink(square.Attributes) || this->blinkingToggle)
		dc.TextOut(x, y, &ch, 1);
}

void CBbsConsole::InvalidateSquare(int row, int column)
{
	ASSERT(row>=0 && row<this->Rows());
	ASSERT(column>=0 && column<this->Columns());

	int l = column*this->charWidth;
	int t = row*this->charHeight;
	int r = (column+1)*this->charWidth;
	int b = (row+1)*this->charHeight;
	InvalidateRect(CRect(l-1,t-1,r+1,b+1), FALSE);
}

void CBbsConsole::InvalidateSquare(const CCoordinate& pt)
{
	InvalidateSquare(pt.row, pt.column);
}

void CBbsConsole::InvalidateBlock(int top, int left, int bottom, int right)
{
	ASSERT(top >= 0 && top < Rows());
	ASSERT(bottom >= 0 && bottom < Rows());
	ASSERT(left >= 0 && left < Columns());
	ASSERT(right >= 0 && right < Columns());

	int l = left*this->charWidth;
	int t = top*this->charHeight;
	int r = (right+1)*this->charWidth;
	int b = (bottom+1)*this->charHeight;
	InvalidateRect(CRect(l,t,r,b), FALSE);
}

bool CBbsConsole::IsWide(wchar_t ch)
{
	return ((ch & 0xFF00) != 0);
}

bool CBbsConsole::IsSelected(int row, int column)
{
	// TODO : IMPLEMENT IT.
	return false;
}
